/* eslint-disable no-empty */
/* eslint-disable @typescript-eslint/no-this-alias */
/* eslint-disable no-redeclare */
/* eslint-disable no-extra-boolean-cast */
/* eslint-disable no-undef */
/* eslint-disable @typescript-eslint/no-empty-function */
/*
 * Released under BSD License
 * Copyright (c) 2014-2016 hizzgdev@163.com
 *
 * Project Home:
 *   https://github.com/hizzgdev/jsmind/
 */

(function ($w) {
    "use strict";
    // set 'jsMind' as the library name.
    // __name__ should be a const value, Never try to change it easily.
    var __name__ = "jsMind";
    // library version
    var __version__ = "0.4.6";
    // author
    var __author__ = "hizzgdev@163.com";

    // an noop function define
    var _noop = function () {};
    var logger =
        typeof console === "undefined"
            ? {
                  log: _noop,
                  debug: _noop,
                  error: _noop,
                  warn: _noop,
                  info: _noop,
              }
            : console;

    // check global variables
    if (typeof module === "undefined" || !module.exports) {
        if (typeof $w[__name__] != "undefined") {
            logger.log(__name__ + " has been already exist.");
            return;
        }
    }

    // shortcut of methods in dom
    var $d = $w.document;
    var $g = function (id) {
        return $d.getElementById(id);
    };
    var $c = function (tag) {
        return $d.createElement(tag);
    };
    var $t = function (n, t) {
        if (n.hasChildNodes()) {
            n.firstChild.nodeValue = t;
        } else {
            n.appendChild($d.createTextNode(t));
        }
    };
    var $h = function (n, t) {
        try {
            n.innerHTML = t;
        } catch (error) {}
    };
    // detect isElement
    var $i = function (el) {
        return (
            !!el &&
            typeof el === "object" &&
            el.nodeType === 1 &&
            typeof el.style === "object" &&
            typeof el.ownerDocument === "object"
        );
    };
    if (typeof String.prototype.startsWith != "function") {
        String.prototype.startsWith = function (p) {
            return this.slice(0, p.length) === p;
        };
    }

    var DEFAULT_OPTIONS = {
        container: "", // id of the container
        editable: false, // you can change it in your options
        theme: null,
        mode: "full", // full or side
        support_html: true,

        view: {
            hmargin: 100,
            vmargin: 50,
            line_width: 2,
            line_color: "#555",
        },
        layout: {
            hspace: 30,
            vspace: 20,
            pspace: 13,
        },
        default_event_handle: {
            enable_mousedown_handle: true,
            enable_click_handle: true,
            enable_dblclick_handle: true,
        },
        shortcut: {
            enable: true,
            handles: {},
            mapping: {
                addchild: 45, // Insert
                addbrother: 13, // Enter
                editnode: 113, // F2
                delnode: 46, // Delete
                toggle: 32, // Space
                left: 37, // Left
                up: 38, // Up
                right: 39, // Right
                down: 40, // Down
            },
        },
    };

    // core object
    var jm = function (options) {
        jm.current = this;

        this.version = __version__;
        var opts = {};
        jm.util.json.merge(opts, DEFAULT_OPTIONS);
        jm.util.json.merge(opts, options);

        if (!opts.container) {
            logger.error("the options.container should not be null or empty.");
            return;
        }
        this.options = opts;
        this.inited = false;
        this.mind = null;
        this.event_handles = [];
        this.init();
    };

    // ============= static object =============================================
    jm.direction = { left: -1, center: 0, right: 1 };
    jm.event_type = { show: 1, resize: 2, edit: 3, select: 4 };

    jm.node = function (
        sId,
        iIndex,
        sTopic,
        oData,
        bIsRoot,
        oParent,
        eDirection,
        bExpanded
    ) {
        if (!sId) {
            logger.error("invalid nodeid");
            return;
        }
        if (typeof iIndex != "number") {
            logger.error("invalid node index");
            return;
        }
        if (typeof bExpanded === "undefined") {
            bExpanded = true;
        }
        this.id = sId;
        this.index = iIndex;
        this.topic = sTopic;
        this.data = oData || {};
        this.isroot = bIsRoot;
        this.parent = oParent;
        this.direction = eDirection;
        this.expanded = !!bExpanded;
        this.children = [];
        this._data = {};
    };

    jm.node.compare = function (node1, node2) {
        // '-1' is alwary the last
        var r = 0;
        var i1 = node1.index;
        var i2 = node2.index;
        if (i1 >= 0 && i2 >= 0) {
            r = i1 - i2;
        } else if (i1 == -1 && i2 == -1) {
            r = 0;
        } else if (i1 == -1) {
            r = 1;
        } else if (i2 == -1) {
            r = -1;
        } else {
            r = 0;
        }
        //logger.debug(i1+' <> '+i2+'  =  '+r);
        return r;
    };

    jm.node.inherited = function (pnode, node) {
        if (!!pnode && !!node) {
            if (pnode.id === node.id) {
                return true;
            }
            if (pnode.isroot) {
                return true;
            }
            var pid = pnode.id;
            var p = node;
            while (!p.isroot) {
                p = p.parent;
                if (p.id === pid) {
                    return true;
                }
            }
        }
        return false;
    };

    jm.node.prototype = {
        get_location: function () {
            var vd = this._data.view;
            return {
                x: vd.abs_x,
                y: vd.abs_y,
            };
        },
        get_size: function () {
            var vd = this._data.view;
            return {
                w: vd.width,
                h: vd.height,
            };
        },
    };

    jm.mind = function () {
        this.name = null;
        this.author = null;
        this.version = null;
        this.root = null;
        this.selected = null;
        this.nodes = {};
    };

    jm.mind.prototype = {
        get_node: function (nodeid) {
            if (nodeid in this.nodes) {
                return this.nodes[nodeid];
            } else {
                logger.warn("the node[id=" + nodeid + "] can not be found");
                return null;
            }
        },

        set_root: function (nodeid, topic, data) {
            if (this.root == null) {
                this.root = new jm.node(nodeid, 0, topic, data, true);
                this._put_node(this.root);
            } else {
                logger.error("root node is already exist");
            }
        },

        add_node: function (
            parent_node,
            nodeid,
            topic,
            data,
            idx,
            direction,
            expanded
        ) {
            if (!jm.util.is_node(parent_node)) {
                var the_parent_node = this.get_node(parent_node);
                if (!the_parent_node) {
                    logger.error(
                        "the parent_node[id=" +
                            parent_node +
                            "] can not be found."
                    );
                    return null;
                } else {
                    return this.add_node(
                        the_parent_node,
                        nodeid,
                        topic,
                        data,
                        idx,
                        direction,
                        expanded
                    );
                }
            }
            var nodeindex = idx || -1;
            var node = null;
            if (parent_node.isroot) {
                var d = jm.direction.right;
                if (isNaN(direction)) {
                    var children = parent_node.children;
                    var children_len = children.length;
                    var r = 0;
                    for (var i = 0; i < children_len; i++) {
                        if (children[i].direction === jm.direction.left) {
                            r--;
                        } else {
                            r++;
                        }
                    }
                    d =
                        children_len > 1 && r > 0
                            ? jm.direction.left
                            : jm.direction.right;
                } else {
                    d =
                        direction != jm.direction.left
                            ? jm.direction.right
                            : jm.direction.left;
                }
                node = new jm.node(
                    nodeid,
                    nodeindex,
                    topic,
                    data,
                    false,
                    parent_node,
                    d,
                    expanded
                );
            } else {
                node = new jm.node(
                    nodeid,
                    nodeindex,
                    topic,
                    data,
                    false,
                    parent_node,
                    parent_node.direction,
                    expanded
                );
            }
            if (this._put_node(node)) {
                parent_node.children.push(node);
                this._reindex(parent_node);
            } else {
                logger.error(
                    "fail, the nodeid '" + node.id + "' has been already exist."
                );
                node = null;
            }
            return node;
        },

        insert_node_before: function (node_before, nodeid, topic, data) {
            if (!jm.util.is_node(node_before)) {
                var the_node_before = this.get_node(node_before);
                if (!the_node_before) {
                    logger.error(
                        "the node_before[id=" +
                            node_before +
                            "] can not be found."
                    );
                    return null;
                } else {
                    return this.insert_node_before(
                        the_node_before,
                        nodeid,
                        topic,
                        data
                    );
                }
            }
            var node_index = node_before.index - 0.5;
            return this.add_node(
                node_before.parent,
                nodeid,
                topic,
                data,
                node_index
            );
        },

        get_node_before: function (node) {
            if (!jm.util.is_node(node)) {
                var the_node = this.get_node(node);
                if (!the_node) {
                    logger.error("the node[id=" + node + "] can not be found.");
                    return null;
                } else {
                    return this.get_node_before(the_node);
                }
            }
            if (node.isroot) {
                return null;
            }
            var idx = node.index - 2;
            if (idx >= 0) {
                return node.parent.children[idx];
            } else {
                return null;
            }
        },

        insert_node_after: function (node_after, nodeid, topic, data) {
            if (!jm.util.is_node(node_after)) {
                var the_node_after = this.get_node(node_before);
                if (!the_node_after) {
                    logger.error(
                        "the node_after[id=" +
                            node_after +
                            "] can not be found."
                    );
                    return null;
                } else {
                    return this.insert_node_after(
                        the_node_after,
                        nodeid,
                        topic,
                        data
                    );
                }
            }
            var node_index = node_after.index + 0.5;
            return this.add_node(
                node_after.parent,
                nodeid,
                topic,
                data,
                node_index
            );
        },

        get_node_after: function (node) {
            if (!jm.util.is_node(node)) {
                var the_node = this.get_node(node);
                if (!the_node) {
                    logger.error("the node[id=" + node + "] can not be found.");
                    return null;
                } else {
                    return this.get_node_after(the_node);
                }
            }
            if (node.isroot) {
                return null;
            }
            var idx = node.index;
            var brothers = node.parent.children;
            if (brothers.length >= idx) {
                return node.parent.children[idx];
            } else {
                return null;
            }
        },

        move_node: function (node, beforeid, parentid, direction) {
            if (!jm.util.is_node(node)) {
                var the_node = this.get_node(node);
                if (!the_node) {
                    logger.error("the node[id=" + node + "] can not be found.");
                    return null;
                } else {
                    return this.move_node(
                        the_node,
                        beforeid,
                        parentid,
                        direction
                    );
                }
            }
            if (!parentid) {
                parentid = node.parent.id;
            }
            return this._move_node(node, beforeid, parentid, direction);
        },

        _flow_node_direction: function (node, direction) {
            if (typeof direction === "undefined") {
                direction = node.direction;
            } else {
                node.direction = direction;
            }
            var len = node.children.length;
            while (len--) {
                this._flow_node_direction(node.children[len], direction);
            }
        },

        _move_node_internal: function (node, beforeid) {
            if (!!node && !!beforeid) {
                if (beforeid == "_last_") {
                    node.index = -1;
                    this._reindex(node.parent);
                } else if (beforeid == "_first_") {
                    node.index = 0;
                    this._reindex(node.parent);
                } else {
                    var node_before = !!beforeid
                        ? this.get_node(beforeid)
                        : null;
                    if (
                        node_before != null &&
                        node_before.parent != null &&
                        node_before.parent.id == node.parent.id
                    ) {
                        node.index = node_before.index - 0.5;
                        this._reindex(node.parent);
                    }
                }
            }
            return node;
        },

        _move_node: function (node, beforeid, parentid, direction) {
            if (!!node && !!parentid) {
                if (node.parent.id != parentid) {
                    // remove from parent's children
                    var sibling = node.parent.children;
                    var si = sibling.length;
                    while (si--) {
                        if (sibling[si].id == node.id) {
                            sibling.splice(si, 1);
                            break;
                        }
                    }
                    node.parent = this.get_node(parentid);
                    node.parent.children.push(node);
                }

                if (node.parent.isroot) {
                    if (direction == jsMind.direction.left) {
                        node.direction = direction;
                    } else {
                        node.direction = jm.direction.right;
                    }
                } else {
                    node.direction = node.parent.direction;
                }
                this._move_node_internal(node, beforeid);
                this._flow_node_direction(node);
            }
            return node;
        },

        remove_node: function (node) {
            if (!jm.util.is_node(node)) {
                var the_node = this.get_node(node);
                if (!the_node) {
                    logger.error("the node[id=" + node + "] can not be found.");
                    return false;
                } else {
                    return this.remove_node(the_node);
                }
            }
            if (!node) {
                logger.error("fail, the node can not be found");
                return false;
            }
            if (node.isroot) {
                logger.error("fail, can not remove root node");
                return false;
            }
            if (this.selected != null && this.selected.id == node.id) {
                this.selected = null;
            }
            // clean all subordinate nodes
            var children = node.children;
            var ci = children.length;
            while (ci--) {
                this.remove_node(children[ci]);
            }
            // clean all children
            children.length = 0;
            // remove from parent's children
            var sibling = node.parent.children;
            var si = sibling.length;
            while (si--) {
                if (sibling[si].id == node.id) {
                    sibling.splice(si, 1);
                    break;
                }
            }
            // remove from global nodes
            delete this.nodes[node.id];
            // clean all properties
            for (var k in node) {
                delete node[k];
            }
            // remove it's self
            node = null;
            //delete node;
            return true;
        },

        _put_node: function (node) {
            if (node.id in this.nodes) {
                logger.warn(
                    "the nodeid '" + node.id + "' has been already exist."
                );
                return false;
            } else {
                this.nodes[node.id] = node;
                return true;
            }
        },

        _reindex: function (node) {
            if (node instanceof jm.node) {
                node.children.sort(jm.node.compare);
                for (var i = 0; i < node.children.length; i++) {
                    node.children[i].index = i + 1;
                }
            }
        },
    };

    jm.format = {
        node_tree: {
            example: {
                meta: {
                    name: __name__,
                    author: __author__,
                    version: __version__,
                },
                format: "node_tree",
                data: { id: "root", topic: "jsMind Example" },
            },
            get_mind: function (source) {
                var df = jm.format.node_tree;
                var mind = new jm.mind();
                mind.name = source.meta.name;
                mind.author = source.meta.author;
                mind.version = source.meta.version;
                df._parse(mind, source.data);
                return mind;
            },
            get_data: function (mind) {
                var df = jm.format.node_tree;
                var json = {};
                json.meta = {
                    name: mind.name,
                    author: mind.author,
                    version: mind.version,
                };
                json.format = "node_tree";
                json.data = df._buildnode(mind.root);
                return json;
            },

            _parse: function (mind, node_root) {
                var df = jm.format.node_tree;
                var data = df._extract_data(node_root);
                mind.set_root(node_root.id, node_root.topic, data);
                if ("children" in node_root) {
                    var children = node_root.children;
                    for (var i = 0; i < children.length; i++) {
                        df._extract_subnode(mind, mind.root, children[i]);
                    }
                }
            },

            _extract_data: function (node_json) {
                var data = {};
                for (var k in node_json) {
                    if (
                        k == "id" ||
                        k == "topic" ||
                        k == "children" ||
                        k == "direction" ||
                        k == "expanded"
                    ) {
                        continue;
                    }
                    data[k] = node_json[k];
                }
                return data;
            },

            _extract_subnode: function (mind, node_parent, node_json) {
                var df = jm.format.node_tree;
                var data = df._extract_data(node_json);
                var d = null;
                if (node_parent.isroot) {
                    d =
                        node_json.direction == "left"
                            ? jm.direction.left
                            : jm.direction.right;
                }
                var node = mind.add_node(
                    node_parent,
                    node_json.id,
                    node_json.topic,
                    data,
                    null,
                    d,
                    node_json.expanded
                );
                if ("children" in node_json) {
                    var children = node_json.children;
                    for (var i = 0; i < children.length; i++) {
                        df._extract_subnode(mind, node, children[i]);
                    }
                }
            },

            _buildnode: function (node) {
                var df = jm.format.node_tree;
                if (!(node instanceof jm.node)) {
                    return;
                }
                var o = {
                    id: node.id,
                    topic: node.topic,
                    expanded: node.expanded,
                };
                if (!!node.parent && node.parent.isroot) {
                    o.direction =
                        node.direction == jm.direction.left ? "left" : "right";
                }
                if (node.data != null) {
                    var node_data = node.data;
                    for (var k in node_data) {
                        o[k] = node_data[k];
                    }
                }
                var children = node.children;
                if (children.length > 0) {
                    o.children = [];
                    for (var i = 0; i < children.length; i++) {
                        o.children.push(df._buildnode(children[i]));
                    }
                }
                return o;
            },
        },

        node_array: {
            example: {
                meta: {
                    name: __name__,
                    author: __author__,
                    version: __version__,
                },
                format: "node_array",
                data: [{ id: "root", topic: "jsMind Example", isroot: true }],
            },

            get_mind: function (source) {
                var df = jm.format.node_array;
                var mind = new jm.mind();
                mind.name = source.meta.name;
                mind.author = source.meta.author;
                mind.version = source.meta.version;
                df._parse(mind, source.data);
                return mind;
            },

            get_data: function (mind) {
                var df = jm.format.node_array;
                var json = {};
                json.meta = {
                    name: mind.name,
                    author: mind.author,
                    version: mind.version,
                };
                json.format = "node_array";
                json.data = [];
                df._array(mind, json.data);
                return json;
            },

            _parse: function (mind, node_array) {
                var df = jm.format.node_array;
                var narray = node_array.slice(0);
                // reverse array for improving looping performance
                narray.reverse();
                var root_id = df._extract_root(mind, narray);
                if (!!root_id) {
                    df._extract_subnode(mind, root_id, narray);
                } else {
                    logger.error("root node can not be found");
                }
            },

            _extract_root: function (mind, node_array) {
                var df = jm.format.node_array;
                var i = node_array.length;
                while (i--) {
                    if ("isroot" in node_array[i] && node_array[i].isroot) {
                        var root_json = node_array[i];
                        var data = df._extract_data(root_json);
                        mind.set_root(root_json.id, root_json.topic, data);
                        node_array.splice(i, 1);
                        return root_json.id;
                    }
                }
                return null;
            },

            _extract_subnode: function (mind, parentid, node_array) {
                var df = jm.format.node_array;
                var i = node_array.length;
                var node_json = null;
                var data = null;
                var extract_count = 0;
                while (i--) {
                    node_json = node_array[i];
                    if (node_json.parentid == parentid) {
                        data = df._extract_data(node_json);
                        var d = null;
                        var node_direction = node_json.direction;
                        if (!!node_direction) {
                            d =
                                node_direction == "left"
                                    ? jm.direction.left
                                    : jm.direction.right;
                        }
                        mind.add_node(
                            parentid,
                            node_json.id,
                            node_json.topic,
                            data,
                            null,
                            d,
                            node_json.expanded
                        );
                        node_array.splice(i, 1);
                        extract_count++;
                        var sub_extract_count = df._extract_subnode(
                            mind,
                            node_json.id,
                            node_array
                        );
                        if (sub_extract_count > 0) {
                            // reset loop index after extract subordinate node
                            i = node_array.length;
                            extract_count += sub_extract_count;
                        }
                    }
                }
                return extract_count;
            },

            _extract_data: function (node_json) {
                var data = {};
                for (var k in node_json) {
                    if (
                        k == "id" ||
                        k == "topic" ||
                        k == "parentid" ||
                        k == "isroot" ||
                        k == "direction" ||
                        k == "expanded"
                    ) {
                        continue;
                    }
                    data[k] = node_json[k];
                }
                return data;
            },

            _array: function (mind, node_array) {
                var df = jm.format.node_array;
                df._array_node(mind.root, node_array);
            },

            _array_node: function (node, node_array) {
                var df = jm.format.node_array;
                if (!(node instanceof jm.node)) {
                    return;
                }
                var o = {
                    id: node.id,
                    topic: node.topic,
                    expanded: node.expanded,
                };
                if (!!node.parent) {
                    o.parentid = node.parent.id;
                }
                if (node.isroot) {
                    o.isroot = true;
                }
                if (!!node.parent && node.parent.isroot) {
                    o.direction =
                        node.direction == jm.direction.left ? "left" : "right";
                }
                if (node.data != null) {
                    var node_data = node.data;
                    for (var k in node_data) {
                        o[k] = node_data[k];
                    }
                }
                node_array.push(o);
                var ci = node.children.length;
                for (var i = 0; i < ci; i++) {
                    df._array_node(node.children[i], node_array);
                }
            },
        },

        freemind: {
            example: {
                meta: {
                    name: __name__,
                    author: __author__,
                    version: __version__,
                },
                format: "freemind",
                data: '<map version="1.0.1"><node ID="root" TEXT="freemind Example"/></map>',
            },
            get_mind: function (source) {
                var df = jm.format.freemind;
                var mind = new jm.mind();
                mind.name = source.meta.name;
                mind.author = source.meta.author;
                mind.version = source.meta.version;
                var xml = source.data;
                var xml_doc = df._parse_xml(xml);
                var xml_root = df._find_root(xml_doc);
                df._load_node(mind, null, xml_root);
                return mind;
            },

            get_data: function (mind) {
                var df = jm.format.freemind;
                var json = {};
                json.meta = {
                    name: mind.name,
                    author: mind.author,
                    version: mind.version,
                };
                json.format = "freemind";
                var xmllines = [];
                xmllines.push('<map version="1.0.1">');
                df._buildmap(mind.root, xmllines);
                xmllines.push("</map>");
                json.data = xmllines.join(" ");
                return json;
            },

            _parse_xml: function (xml) {
                var xml_doc = null;
                if (window.DOMParser) {
                    var parser = new DOMParser();
                    xml_doc = parser.parseFromString(xml, "text/xml");
                } else {
                    // Internet Explorer
                    xml_doc = new ActiveXObject("Microsoft.XMLDOM");
                    xml_doc.async = false;
                    xml_doc.loadXML(xml);
                }
                return xml_doc;
            },

            _find_root: function (xml_doc) {
                var nodes = xml_doc.childNodes;
                var node = null;
                var root = null;
                var n = null;
                for (var i = 0; i < nodes.length; i++) {
                    n = nodes[i];
                    if (n.nodeType == 1 && n.tagName == "map") {
                        node = n;
                        break;
                    }
                }
                if (!!node) {
                    var ns = node.childNodes;
                    node = null;
                    for (var i = 0; i < ns.length; i++) {
                        n = ns[i];
                        if (n.nodeType == 1 && n.tagName == "node") {
                            node = n;
                            break;
                        }
                    }
                }
                return node;
            },

            _load_node: function (mind, parent_id, xml_node) {
                var df = jm.format.freemind;
                var node_id = xml_node.getAttribute("ID");
                var node_topic = xml_node.getAttribute("TEXT");
                // look for richcontent
                if (node_topic == null) {
                    var topic_children = xml_node.childNodes;
                    var topic_child = null;
                    for (var i = 0; i < topic_children.length; i++) {
                        topic_child = topic_children[i];
                        //logger.debug(topic_child.tagName);
                        if (
                            topic_child.nodeType == 1 &&
                            topic_child.tagName === "richcontent"
                        ) {
                            node_topic = topic_child.textContent;
                            break;
                        }
                    }
                }
                var node_data = df._load_attributes(xml_node);
                var node_expanded =
                    "expanded" in node_data
                        ? node_data.expanded == "true"
                        : true;
                delete node_data.expanded;

                var node_position = xml_node.getAttribute("POSITION");
                var node_direction = null;
                if (!!node_position) {
                    node_direction =
                        node_position == "left"
                            ? jm.direction.left
                            : jm.direction.right;
                }
                //logger.debug(node_position +':'+ node_direction);
                if (!!parent_id) {
                    mind.add_node(
                        parent_id,
                        node_id,
                        node_topic,
                        node_data,
                        null,
                        node_direction,
                        node_expanded
                    );
                } else {
                    mind.set_root(node_id, node_topic, node_data);
                }
                var children = xml_node.childNodes;
                var child = null;
                for (var i = 0; i < children.length; i++) {
                    child = children[i];
                    if (child.nodeType == 1 && child.tagName == "node") {
                        df._load_node(mind, node_id, child);
                    }
                }
            },

            _load_attributes: function (xml_node) {
                var children = xml_node.childNodes;
                var attr = null;
                var attr_data = {};
                for (var i = 0; i < children.length; i++) {
                    attr = children[i];
                    if (attr.nodeType == 1 && attr.tagName === "attribute") {
                        attr_data[attr.getAttribute("NAME")] =
                            attr.getAttribute("VALUE");
                    }
                }
                return attr_data;
            },

            _buildmap: function (node, xmllines) {
                var df = jm.format.freemind;
                var pos = null;
                if (!!node.parent && node.parent.isroot) {
                    pos =
                        node.direction === jm.direction.left ? "left" : "right";
                }
                xmllines.push("<node");
                xmllines.push('ID="' + node.id + '"');
                if (!!pos) {
                    xmllines.push('POSITION="' + pos + '"');
                }
                xmllines.push('TEXT="' + node.topic + '">');

                // store expanded status as an attribute
                xmllines.push(
                    '<attribute NAME="expanded" VALUE="' + node.expanded + '"/>'
                );

                // for attributes
                var node_data = node.data;
                if (node_data != null) {
                    for (var k in node_data) {
                        xmllines.push(
                            '<attribute NAME="' +
                                k +
                                '" VALUE="' +
                                node_data[k] +
                                '"/>'
                        );
                    }
                }

                // for children
                var children = node.children;
                for (var i = 0; i < children.length; i++) {
                    df._buildmap(children[i], xmllines);
                }

                xmllines.push("</node>");
            },
        },
    };

    // ============= utility object =============================================

    jm.util = {
        is_node: function (node) {
            return !!node && node instanceof jm.node;
        },
        ajax: {
            _xhr: function () {
                var xhr = null;
                if (window.XMLHttpRequest) {
                    xhr = new XMLHttpRequest();
                } else {
                    try {
                        xhr = new ActiveXObject("Microsoft.XMLHTTP");
                    } catch (e) {}
                }
                return xhr;
            },
            _eurl: function (url) {
                return encodeURIComponent(url);
            },
            request: function (url, param, method, callback, fail_callback) {
                var a = jm.util.ajax;
                var p = null;
                var tmp_param = [];
                for (var k in param) {
                    tmp_param.push(a._eurl(k) + "=" + a._eurl(param[k]));
                }
                if (tmp_param.length > 0) {
                    p = tmp_param.join("&");
                }
                var xhr = a._xhr();
                if (!xhr) {
                    return;
                }
                xhr.onreadystatechange = function () {
                    if (xhr.readyState == 4) {
                        if (xhr.status == 200 || xhr.status == 0) {
                            if (typeof callback === "function") {
                                var data = jm.util.json.string2json(
                                    xhr.responseText
                                );
                                if (data != null) {
                                    callback(data);
                                } else {
                                    callback(xhr.responseText);
                                }
                            }
                        } else {
                            if (typeof fail_callback === "function") {
                                fail_callback(xhr);
                            } else {
                                logger.error("xhr request failed.", xhr);
                            }
                        }
                    }
                };
                method = method || "GET";
                xhr.open(method, url, true);
                xhr.setRequestHeader("If-Modified-Since", "0");
                if (method == "POST") {
                    xhr.setRequestHeader(
                        "Content-Type",
                        "application/x-www-form-urlencoded;charset=utf-8"
                    );
                    xhr.send(p);
                } else {
                    xhr.send();
                }
            },
            get: function (url, callback) {
                return jm.util.ajax.request(url, {}, "GET", callback);
            },
            post: function (url, param, callback) {
                return jm.util.ajax.request(url, param, "POST", callback);
            },
        },

        dom: {
            //target,eventType,handler
            add_event: function (t, e, h) {
                if (!!t.addEventListener) {
                    t.addEventListener(e, h, false);
                } else {
                    t.attachEvent("on" + e, h);
                }
            },
        },

        canvas: {
            bezierto: function (ctx, x1, y1, x2, y2) {
                ctx.beginPath();
                ctx.moveTo(x1, y1);
                ctx.bezierCurveTo(x1 + ((x2 - x1) * 2) / 3, y1, x1, y2, x2, y2);
                ctx.stroke();
            },
            lineto: function (ctx, x1, y1, x2, y2) {
                ctx.beginPath();
                ctx.moveTo(x1, y1);
                ctx.lineTo(x2, y2);
                ctx.stroke();
            },
            clear: function (ctx, x, y, w, h) {
                ctx.clearRect(x, y, w, h);
            },
        },

        file: {
            read: function (file_data, fn_callback) {
                var reader = new FileReader();
                reader.onload = function () {
                    if (typeof fn_callback === "function") {
                        fn_callback(this.result, file_data.name);
                    }
                };
                reader.readAsText(file_data);
            },

            save: function (file_data, type, name) {
                var blob;
                if (typeof $w.Blob === "function") {
                    blob = new Blob([file_data], { type: type });
                } else {
                    var BlobBuilder =
                        $w.BlobBuilder ||
                        $w.MozBlobBuilder ||
                        $w.WebKitBlobBuilder ||
                        $w.MSBlobBuilder;
                    var bb = new BlobBuilder();
                    bb.append(file_data);
                    blob = bb.getBlob(type);
                }
                if (navigator.msSaveBlob) {
                    navigator.msSaveBlob(blob, name);
                } else {
                    var URL = $w.URL || $w.webkitURL;
                    var bloburl = URL.createObjectURL(blob);
                    var anchor = $c("a");
                    if ("download" in anchor) {
                        anchor.style.visibility = "hidden";
                        anchor.href = bloburl;
                        anchor.download = name;
                        $d.body.appendChild(anchor);
                        var evt = $d.createEvent("MouseEvents");
                        evt.initEvent("click", true, true);
                        anchor.dispatchEvent(evt);
                        $d.body.removeChild(anchor);
                    } else {
                        location.href = bloburl;
                    }
                }
            },
        },

        json: {
            json2string: function (json) {
                if (!!JSON) {
                    try {
                        var json_str = JSON.stringify(json);
                        return json_str;
                    } catch (e) {
                        logger.warn(e);
                        logger.warn("can not convert to string");
                        return null;
                    }
                }
            },
            string2json: function (json_str) {
                if (!!JSON) {
                    try {
                        var json = JSON.parse(json_str);
                        return json;
                    } catch (e) {
                        logger.warn(e);
                        logger.warn("can not parse to json");
                        return null;
                    }
                }
            },
            merge: function (b, a) {
                for (var o in a) {
                    if (o in b) {
                        if (
                            typeof b[o] === "object" &&
                            Object.prototype.toString
                                .call(b[o])
                                .toLowerCase() == "[object object]" &&
                            !b[o].length
                        ) {
                            jm.util.json.merge(b[o], a[o]);
                        } else {
                            b[o] = a[o];
                        }
                    } else {
                        b[o] = a[o];
                    }
                }
                return b;
            },
        },

        uuid: {
            newid: function () {
                return (
                    new Date().getTime().toString(16) +
                    Math.random().toString(16).substr(2)
                ).substr(2, 16);
            },
        },

        text: {
            is_empty: function (s) {
                if (!s) {
                    return true;
                }
                return s.replace(/\s*/, "").length == 0;
            },
        },
    };

    jm.prototype = {
        init: function () {
            if (this.inited) {
                return;
            }
            this.inited = true;

            var opts = this.options;

            var opts_layout = {
                mode: opts.mode,
                hspace: opts.layout.hspace,
                vspace: opts.layout.vspace,
                pspace: opts.layout.pspace,
            };
            var opts_view = {
                container: opts.container,
                support_html: opts.support_html,
                hmargin: opts.view.hmargin,
                vmargin: opts.view.vmargin,
                line_width: opts.view.line_width,
                line_color: opts.view.line_color,
            };
            // create instance of function provider
            this.data = new jm.data_provider(this);
            this.layout = new jm.layout_provider(this, opts_layout);
            this.view = new jm.view_provider(this, opts_view);
            this.shortcut = new jm.shortcut_provider(this, opts.shortcut);

            this.data.init();
            this.layout.init();
            this.view.init();
            this.shortcut.init();

            this._event_bind();

            jm.init_plugins(this);
        },

        enable_edit: function () {
            this.options.editable = true;
        },

        disable_edit: function () {
            this.options.editable = false;
        },

        // call enable_event_handle('dblclick')
        // options are 'mousedown', 'click', 'dblclick'
        enable_event_handle: function (event_handle) {
            this.options.default_event_handle[
                "enable_" + event_handle + "_handle"
            ] = true;
        },

        // call disable_event_handle('dblclick')
        // options are 'mousedown', 'click', 'dblclick'
        disable_event_handle: function (event_handle) {
            this.options.default_event_handle[
                "enable_" + event_handle + "_handle"
            ] = false;
        },

        get_editable: function () {
            return this.options.editable;
        },

        set_theme: function (theme) {
            var theme_old = this.options.theme;
            this.options.theme = !!theme ? theme : null;
            if (theme_old != this.options.theme) {
                this.view.reset_theme();
                this.view.reset_custom_style();
            }
        },
        _event_bind: function () {
            this.view.add_event(this, "mousedown", this.mousedown_handle);
            this.view.add_event(this, "click", this.click_handle);
            this.view.add_event(this, "dblclick", this.dblclick_handle);
        },

        mousedown_handle: function (e) {
            if (!this.options.default_event_handle["enable_mousedown_handle"]) {
                return;
            }
            var element = e.target || event.srcElement;
            var nodeid = this.view.get_binded_nodeid(element);
            if (!!nodeid) {
                this.select_node(nodeid);
            } else {
                this.select_clear();
            }
        },

        click_handle: function (e) {
            if (!this.options.default_event_handle["enable_click_handle"]) {
                return;
            }
            var element = e.target || event.srcElement;
            var isexpander = this.view.is_expander(element);
            if (isexpander) {
                var nodeid = this.view.get_binded_nodeid(element);
                if (!!nodeid) {
                    this.toggle_node(nodeid);
                }
            }
        },

        dblclick_handle: function (e) {
            if (!this.options.default_event_handle["enable_dblclick_handle"]) {
                return;
            }
            if (this.get_editable()) {
                var element = e.target || event.srcElement;
                var nodeid = this.view.get_binded_nodeid(element);
                if (!!nodeid) {
                    this.begin_edit(nodeid);
                }
            }
        },

        begin_edit: function (node) {
            if (!jm.util.is_node(node)) {
                var the_node = this.get_node(node);
                if (!the_node) {
                    logger.error("the node[id=" + node + "] can not be found.");
                    return false;
                } else {
                    return this.begin_edit(the_node);
                }
            }
            if (this.get_editable()) {
                this.view.edit_node_begin(node);
            } else {
                logger.error("fail, this mind map is not editable.");
                return;
            }
        },

        end_edit: function () {
            this.view.edit_node_end();
        },

        toggle_node: function (node) {
            if (!jm.util.is_node(node)) {
                var the_node = this.get_node(node);
                if (!the_node) {
                    logger.error("the node[id=" + node + "] can not be found.");
                    return;
                } else {
                    return this.toggle_node(the_node);
                }
            }
            if (node.isroot) {
                return;
            }
            this.view.save_location(node);
            this.layout.toggle_node(node);
            this.view.relayout();
            this.view.restore_location(node);
        },

        expand_node: function (node) {
            if (!jm.util.is_node(node)) {
                var the_node = this.get_node(node);
                if (!the_node) {
                    logger.error("the node[id=" + node + "] can not be found.");
                    return;
                } else {
                    return this.expand_node(the_node);
                }
            }
            if (node.isroot) {
                return;
            }
            this.view.save_location(node);
            this.layout.expand_node(node);
            this.view.relayout();
            this.view.restore_location(node);
        },

        collapse_node: function (node) {
            if (!jm.util.is_node(node)) {
                var the_node = this.get_node(node);
                if (!the_node) {
                    logger.error("the node[id=" + node + "] can not be found.");
                    return;
                } else {
                    return this.collapse_node(the_node);
                }
            }
            if (node.isroot) {
                return;
            }
            this.view.save_location(node);
            this.layout.collapse_node(node);
            this.view.relayout();
            this.view.restore_location(node);
        },

        expand_all: function () {
            this.layout.expand_all();
            this.view.relayout();
        },

        collapse_all: function () {
            this.layout.collapse_all();
            this.view.relayout();
        },

        expand_to_depth: function (depth) {
            this.layout.expand_to_depth(depth);
            this.view.relayout();
        },

        _reset: function () {
            this.view.reset();
            this.layout.reset();
            this.data.reset();
        },

        _show: function (mind) {
            var m = mind || jm.format.node_array.example;

            this.mind = this.data.load(m);
            if (!this.mind) {
                logger.error("data.load error");
                return;
            } else {
                logger.debug("data.load ok");
            }

            this.view.load();
            logger.debug("view.load ok");

            this.layout.layout();
            logger.debug("layout.layout ok");

            this.view.show(true);
            logger.debug("view.show ok");

            this.invoke_event_handle(jm.event_type.show, { data: [mind] });
        },

        show: function (mind) {
            this._reset();
            this._show(mind);
        },

        get_meta: function () {
            return {
                name: this.mind.name,
                author: this.mind.author,
                version: this.mind.version,
            };
        },

        get_data: function (data_format) {
            var df = data_format || "node_tree";
            return this.data.get_data(df);
        },

        get_root: function () {
            return this.mind.root;
        },

        get_node: function (nodeid) {
            return this.mind.get_node(nodeid);
        },

        add_node: function (parent_node, nodeid, topic, data) {
            if (this.get_editable()) {
                var node = this.mind.add_node(parent_node, nodeid, topic, data);
                if (!!node) {
                    this.view.add_node(node);
                    this.layout.layout();
                    this.view.show(false);
                    this.view.reset_node_custom_style(node);
                    this.expand_node(parent_node);
                    this.invoke_event_handle(jm.event_type.edit, {
                        evt: "add_node",
                        data: [parent_node.id, nodeid, topic, data],
                        node: nodeid,
                    });
                }
                return node;
            } else {
                logger.error("fail, this mind map is not editable");
                return null;
            }
        },

        insert_node_before: function (node_before, nodeid, topic, data) {
            if (this.get_editable()) {
                var beforeid = jm.util.is_node(node_before)
                    ? node_before.id
                    : node_before;
                var node = this.mind.insert_node_before(
                    node_before,
                    nodeid,
                    topic,
                    data
                );
                if (!!node) {
                    this.view.add_node(node);
                    this.layout.layout();
                    this.view.show(false);
                    this.invoke_event_handle(jm.event_type.edit, {
                        evt: "insert_node_before",
                        data: [beforeid, nodeid, topic, data],
                        node: nodeid,
                    });
                }
                return node;
            } else {
                logger.error("fail, this mind map is not editable");
                return null;
            }
        },

        insert_node_after: function (node_after, nodeid, topic, data) {
            if (this.get_editable()) {
                var afterid = jm.util.is_node(node_after)
                    ? node_after.id
                    : node_after;
                var node = this.mind.insert_node_after(
                    node_after,
                    nodeid,
                    topic,
                    data
                );
                if (!!node) {
                    this.view.add_node(node);
                    this.layout.layout();
                    this.view.show(false);
                    this.invoke_event_handle(jm.event_type.edit, {
                        evt: "insert_node_after",
                        data: [afterid, nodeid, topic, data],
                        node: nodeid,
                    });
                }
                return node;
            } else {
                logger.error("fail, this mind map is not editable");
                return null;
            }
        },

        remove_node: function (node) {
            if (!jm.util.is_node(node)) {
                var the_node = this.get_node(node);
                if (!the_node) {
                    logger.error("the node[id=" + node + "] can not be found.");
                    return false;
                } else {
                    return this.remove_node(the_node);
                }
            }
            if (this.get_editable()) {
                if (node.isroot) {
                    logger.error("fail, can not remove root node");
                    return false;
                }
                var nodeid = node.id;
                var parentid = node.parent.id;
                var parent_node = this.get_node(parentid);
                this.view.save_location(parent_node);
                this.view.remove_node(node);
                this.mind.remove_node(node);
                this.layout.layout();
                this.view.show(false);
                this.view.restore_location(parent_node);
                this.invoke_event_handle(jm.event_type.edit, {
                    evt: "remove_node",
                    data: [nodeid],
                    node: parentid,
                });
                return true;
            } else {
                logger.error("fail, this mind map is not editable");
                return false;
            }
        },

        update_node: function (nodeid, topic) {
            if (this.get_editable()) {
                if (jm.util.text.is_empty(topic)) {
                    logger.warn("fail, topic can not be empty");
                    return;
                }
                var node = this.get_node(nodeid);
                if (!!node) {
                    if (node.topic === topic) {
                        logger.info("nothing changed");
                        this.view.update_node(node);
                        return;
                    }
                    node.topic = topic;
                    this.view.update_node(node);
                    this.layout.layout();
                    this.view.show(false);
                    this.invoke_event_handle(jm.event_type.edit, {
                        evt: "update_node",
                        data: [nodeid, topic],
                        node: nodeid,
                    });
                }
            } else {
                logger.error("fail, this mind map is not editable");
                return;
            }
        },

        move_node: function (nodeid, beforeid, parentid, direction) {
            if (this.get_editable()) {
                var node = this.mind.move_node(
                    nodeid,
                    beforeid,
                    parentid,
                    direction
                );
                if (!!node) {
                    this.view.update_node(node);
                    this.layout.layout();
                    this.view.show(false);
                    this.invoke_event_handle(jm.event_type.edit, {
                        evt: "move_node",
                        data: [nodeid, beforeid, parentid, direction],
                        node: nodeid,
                    });
                }
            } else {
                logger.error("fail, this mind map is not editable");
                return;
            }
        },

        select_node: function (node) {
            if (!jm.util.is_node(node)) {
                var the_node = this.get_node(node);
                if (!the_node) {
                    logger.error("the node[id=" + node + "] can not be found.");
                    return;
                } else {
                    return this.select_node(the_node);
                }
            }
            if (!this.layout.is_visible(node)) {
                return;
            }
            this.mind.selected = node;
            this.view.select_node(node);
        },

        get_selected_node: function () {
            if (!!this.mind) {
                return this.mind.selected;
            } else {
                return null;
            }
        },

        select_clear: function () {
            if (!!this.mind) {
                this.mind.selected = null;
                this.view.select_clear();
            }
        },

        is_node_visible: function (node) {
            return this.layout.is_visible(node);
        },

        find_node_before: function (node) {
            if (!jm.util.is_node(node)) {
                var the_node = this.get_node(node);
                if (!the_node) {
                    logger.error("the node[id=" + node + "] can not be found.");
                    return;
                } else {
                    return this.find_node_before(the_node);
                }
            }
            if (node.isroot) {
                return null;
            }
            var n = null;
            if (node.parent.isroot) {
                var c = node.parent.children;
                var prev = null;
                var ni = null;
                for (var i = 0; i < c.length; i++) {
                    ni = c[i];
                    if (node.direction === ni.direction) {
                        if (node.id === ni.id) {
                            n = prev;
                        }
                        prev = ni;
                    }
                }
            } else {
                n = this.mind.get_node_before(node);
            }
            return n;
        },

        find_node_after: function (node) {
            if (!jm.util.is_node(node)) {
                var the_node = this.get_node(node);
                if (!the_node) {
                    logger.error("the node[id=" + node + "] can not be found.");
                    return;
                } else {
                    return this.find_node_after(the_node);
                }
            }
            if (node.isroot) {
                return null;
            }
            var n = null;
            if (node.parent.isroot) {
                var c = node.parent.children;
                var getthis = false;
                var ni = null;
                for (var i = 0; i < c.length; i++) {
                    ni = c[i];
                    if (node.direction === ni.direction) {
                        if (getthis) {
                            n = ni;
                            break;
                        }
                        if (node.id === ni.id) {
                            getthis = true;
                        }
                    }
                }
            } else {
                n = this.mind.get_node_after(node);
            }
            return n;
        },

        set_node_color: function (nodeid, bgcolor, fgcolor) {
            if (this.get_editable()) {
                var node = this.mind.get_node(nodeid);
                if (!!node) {
                    if (!!bgcolor) {
                        node.data["background-color"] = bgcolor;
                    }
                    if (!!fgcolor) {
                        node.data["foreground-color"] = fgcolor;
                    }
                    this.view.reset_node_custom_style(node);
                }
            } else {
                logger.error("fail, this mind map is not editable");
                return null;
            }
        },

        set_node_font_style: function (nodeid, size, weight, style) {
            if (this.get_editable()) {
                var node = this.mind.get_node(nodeid);
                if (!!node) {
                    if (!!size) {
                        node.data["font-size"] = size;
                    }
                    if (!!weight) {
                        node.data["font-weight"] = weight;
                    }
                    if (!!style) {
                        node.data["font-style"] = style;
                    }
                    this.view.reset_node_custom_style(node);
                    this.view.update_node(node);
                    this.layout.layout();
                    this.view.show(false);
                }
            } else {
                logger.error("fail, this mind map is not editable");
                return null;
            }
        },

        set_node_background_image: function (
            nodeid,
            image,
            width,
            height,
            rotation
        ) {
            if (this.get_editable()) {
                var node = this.mind.get_node(nodeid);
                if (!!node) {
                    if (!!image) {
                        node.data["background-image"] = image;
                    }
                    if (!!width) {
                        node.data["width"] = width;
                    }
                    if (!!height) {
                        node.data["height"] = height;
                    }
                    if (!!rotation) {
                        node.data["background-rotation"] = rotation;
                    }
                    this.view.reset_node_custom_style(node);
                    this.view.update_node(node);
                    this.layout.layout();
                    this.view.show(false);
                }
            } else {
                logger.error("fail, this mind map is not editable");
                return null;
            }
        },

        set_node_background_rotation: function (nodeid, rotation) {
            if (this.get_editable()) {
                var node = this.mind.get_node(nodeid);
                if (!!node) {
                    if (!node.data["background-image"]) {
                        logger.error(
                            "fail, only can change rotation angle of node with background image"
                        );
                        return null;
                    }
                    node.data["background-rotation"] = rotation;
                    this.view.reset_node_custom_style(node);
                    this.view.update_node(node);
                    this.layout.layout();
                    this.view.show(false);
                }
            } else {
                logger.error("fail, this mind map is not editable");
                return null;
            }
        },

        resize: function () {
            this.view.resize();
        },

        // callback(type ,data)
        add_event_listener: function (callback) {
            if (typeof callback === "function") {
                this.event_handles.push(callback);
            }
        },

        invoke_event_handle: function (type, data) {
            var j = this;
            $w.setTimeout(function () {
                j._invoke_event_handle(type, data);
            }, 0);
        },

        _invoke_event_handle: function (type, data) {
            var l = this.event_handles.length;
            for (var i = 0; i < l; i++) {
                this.event_handles[i](type, data);
            }
        },
    };

    // ============= data provider =============================================

    jm.data_provider = function (jm) {
        this.jm = jm;
    };

    jm.data_provider.prototype = {
        init: function () {
            logger.debug("data.init");
        },

        reset: function () {
            logger.debug("data.reset");
        },

        load: function (mind_data) {
            var df = null;
            var mind = null;
            if (typeof mind_data === "object") {
                if (!!mind_data.format) {
                    df = mind_data.format;
                } else {
                    df = "node_tree";
                }
            } else {
                df = "freemind";
            }

            if (df == "node_array") {
                mind = jm.format.node_array.get_mind(mind_data);
            } else if (df == "node_tree") {
                mind = jm.format.node_tree.get_mind(mind_data);
            } else if (df == "freemind") {
                mind = jm.format.freemind.get_mind(mind_data);
            } else {
                logger.warn("unsupported format");
            }
            return mind;
        },

        get_data: function (data_format) {
            var data = null;
            if (data_format == "node_array") {
                data = jm.format.node_array.get_data(this.jm.mind);
            } else if (data_format == "node_tree") {
                data = jm.format.node_tree.get_data(this.jm.mind);
            } else if (data_format == "freemind") {
                data = jm.format.freemind.get_data(this.jm.mind);
            } else {
                logger.error("unsupported " + data_format + " format");
            }
            return data;
        },
    };

    // ============= layout provider ===========================================

    jm.layout_provider = function (jm, options) {
        this.opts = options;
        this.jm = jm;
        this.isside = this.opts.mode == "side";
        this.bounds = null;

        this.cache_valid = false;
    };

    jm.layout_provider.prototype = {
        init: function () {
            logger.debug("layout.init");
        },
        reset: function () {
            logger.debug("layout.reset");
            this.bounds = { n: 0, s: 0, w: 0, e: 0 };
        },
        layout: function () {
            logger.debug("layout.layout");
            this.layout_direction();
            this.layout_offset();
        },

        layout_direction: function () {
            this._layout_direction_root();
        },

        _layout_direction_root: function () {
            var node = this.jm.mind.root;
            // logger.debug(node);
            var layout_data = null;
            if ("layout" in node._data) {
                layout_data = node._data.layout;
            } else {
                layout_data = {};
                node._data.layout = layout_data;
            }
            var children = node.children;
            var children_count = children.length;
            layout_data.direction = jm.direction.center;
            layout_data.side_index = 0;
            if (this.isside) {
                var i = children_count;
                while (i--) {
                    this._layout_direction_side(
                        children[i],
                        jm.direction.right,
                        i
                    );
                }
            } else {
                var i = children_count;
                var subnode = null;
                while (i--) {
                    subnode = children[i];
                    if (subnode.direction == jm.direction.left) {
                        this._layout_direction_side(
                            subnode,
                            jm.direction.left,
                            i
                        );
                    } else {
                        this._layout_direction_side(
                            subnode,
                            jm.direction.right,
                            i
                        );
                    }
                }
                /*
                var boundary = Math.ceil(children_count/2);
                var i = children_count;
                while(i--){
                    if(i>=boundary){
                        this._layout_direction_side(children[i],jm.direction.left, children_count-i-1);
                    }else{
                        this._layout_direction_side(children[i],jm.direction.right, i);
                    }
                }*/
            }
        },

        _layout_direction_side: function (node, direction, side_index) {
            var layout_data = null;
            if ("layout" in node._data) {
                layout_data = node._data.layout;
            } else {
                layout_data = {};
                node._data.layout = layout_data;
            }
            var children = node.children;
            var children_count = children.length;

            layout_data.direction = direction;
            layout_data.side_index = side_index;
            var i = children_count;
            while (i--) {
                this._layout_direction_side(children[i], direction, i);
            }
        },

        layout_offset: function () {
            var node = this.jm.mind.root;
            var layout_data = node._data.layout;
            layout_data.offset_x = 0;
            layout_data.offset_y = 0;
            layout_data.outer_height = 0;
            var children = node.children;
            var i = children.length;
            var left_nodes = [];
            var right_nodes = [];
            var subnode = null;
            while (i--) {
                subnode = children[i];
                if (subnode._data.layout.direction == jm.direction.right) {
                    right_nodes.unshift(subnode);
                } else {
                    left_nodes.unshift(subnode);
                }
            }
            layout_data.left_nodes = left_nodes;
            layout_data.right_nodes = right_nodes;
            layout_data.outer_height_left =
                this._layout_offset_subnodes(left_nodes);
            layout_data.outer_height_right =
                this._layout_offset_subnodes(right_nodes);
            this.bounds.e = node._data.view.width / 2;
            this.bounds.w = 0 - this.bounds.e;
            //logger.debug(this.bounds.w);
            this.bounds.n = 0;
            this.bounds.s = Math.max(
                layout_data.outer_height_left,
                layout_data.outer_height_right
            );
        },

        // layout both the x and y axis
        _layout_offset_subnodes: function (nodes) {
            var total_height = 0;
            var nodes_count = nodes.length;
            var i = nodes_count;
            var node = null;
            var node_outer_height = 0;
            var layout_data = null;
            var base_y = 0;
            var pd = null; // parent._data
            while (i--) {
                node = nodes[i];
                layout_data = node._data.layout;
                if (pd == null) {
                    pd = node.parent._data;
                }

                node_outer_height = this._layout_offset_subnodes(node.children);
                if (!node.expanded) {
                    node_outer_height = 0;
                    this.set_visible(node.children, false);
                }
                node_outer_height = Math.max(
                    node._data.view.height,
                    node_outer_height
                );

                layout_data.outer_height = node_outer_height;
                layout_data.offset_y = base_y - node_outer_height / 2;
                layout_data.offset_x =
                    this.opts.hspace * layout_data.direction +
                    (pd.view.width *
                        (pd.layout.direction + layout_data.direction)) /
                        2;
                if (!node.parent.isroot) {
                    layout_data.offset_x +=
                        this.opts.pspace * layout_data.direction;
                }

                base_y = base_y - node_outer_height - this.opts.vspace;
                total_height += node_outer_height;
            }
            if (nodes_count > 1) {
                total_height += this.opts.vspace * (nodes_count - 1);
            }
            i = nodes_count;
            var middle_height = total_height / 2;
            while (i--) {
                node = nodes[i];
                node._data.layout.offset_y += middle_height;
            }
            return total_height;
        },

        // layout the y axis only, for collapse/expand a node
        _layout_offset_subnodes_height: function (nodes) {
            var total_height = 0;
            var nodes_count = nodes.length;
            var i = nodes_count;
            var node = null;
            var node_outer_height = 0;
            var layout_data = null;
            var base_y = 0;
            var pd = null; // parent._data
            while (i--) {
                node = nodes[i];
                layout_data = node._data.layout;
                if (pd == null) {
                    pd = node.parent._data;
                }

                node_outer_height = this._layout_offset_subnodes_height(
                    node.children
                );
                if (!node.expanded) {
                    node_outer_height = 0;
                }
                node_outer_height = Math.max(
                    node._data.view.height,
                    node_outer_height
                );

                layout_data.outer_height = node_outer_height;
                layout_data.offset_y = base_y - node_outer_height / 2;
                base_y = base_y - node_outer_height - this.opts.vspace;
                total_height += node_outer_height;
            }
            if (nodes_count > 1) {
                total_height += this.opts.vspace * (nodes_count - 1);
            }
            i = nodes_count;
            var middle_height = total_height / 2;
            while (i--) {
                node = nodes[i];
                node._data.layout.offset_y += middle_height;
                //logger.debug(node.topic);
                //logger.debug(node._data.layout.offset_y);
            }
            return total_height;
        },

        get_node_offset: function (node) {
            var layout_data = node._data.layout;
            var offset_cache = null;
            if ("_offset_" in layout_data && this.cache_valid) {
                offset_cache = layout_data._offset_;
            } else {
                offset_cache = { x: -1, y: -1 };
                layout_data._offset_ = offset_cache;
            }
            if (offset_cache.x == -1 || offset_cache.y == -1) {
                var x = layout_data.offset_x;
                var y = layout_data.offset_y;
                if (!node.isroot) {
                    var offset_p = this.get_node_offset(node.parent);
                    x += offset_p.x;
                    y += offset_p.y;
                }
                offset_cache.x = x;
                offset_cache.y = y;
            }
            return offset_cache;
        },

        get_node_point: function (node) {
            var view_data = node._data.view;
            var offset_p = this.get_node_offset(node);
            //logger.debug(offset_p);
            var p = {};
            p.x =
                offset_p.x +
                (view_data.width * (node._data.layout.direction - 1)) / 2;
            p.y = offset_p.y - view_data.height / 2;
            //logger.debug(p);
            return p;
        },

        get_node_point_in: function (node) {
            var p = this.get_node_offset(node);
            return p;
        },

        get_node_point_out: function (node) {
            var layout_data = node._data.layout;
            var pout_cache = null;
            if ("_pout_" in layout_data && this.cache_valid) {
                pout_cache = layout_data._pout_;
            } else {
                pout_cache = { x: -1, y: -1 };
                layout_data._pout_ = pout_cache;
            }
            if (pout_cache.x == -1 || pout_cache.y == -1) {
                if (node.isroot) {
                    pout_cache.x = 0;
                    pout_cache.y = 0;
                } else {
                    var view_data = node._data.view;
                    var offset_p = this.get_node_offset(node);
                    pout_cache.x =
                        offset_p.x +
                        (view_data.width + this.opts.pspace) *
                            node._data.layout.direction;
                    pout_cache.y = offset_p.y;
                    //logger.debug('pout');
                    //logger.debug(pout_cache);
                }
            }
            return pout_cache;
        },

        get_expander_point: function (node) {
            var p = this.get_node_point_out(node);
            var ex_p = {};
            if (node._data.layout.direction == jm.direction.right) {
                ex_p.x = p.x - this.opts.pspace;
            } else {
                ex_p.x = p.x;
            }
            ex_p.y = p.y - Math.ceil(this.opts.pspace / 2);
            return ex_p;
        },

        get_min_size: function () {
            var nodes = this.jm.mind.nodes;
            var node = null;
            var pout = null;
            for (var nodeid in nodes) {
                node = nodes[nodeid];
                pout = this.get_node_point_out(node);
                //logger.debug(pout.x);
                if (pout.x > this.bounds.e) {
                    this.bounds.e = pout.x;
                }
                if (pout.x < this.bounds.w) {
                    this.bounds.w = pout.x;
                }
            }
            return {
                w: this.bounds.e - this.bounds.w,
                h: this.bounds.s - this.bounds.n,
            };
        },

        toggle_node: function (node) {
            if (node.isroot) {
                return;
            }
            if (node.expanded) {
                this.collapse_node(node);
            } else {
                this.expand_node(node);
            }
        },

        expand_node: function (node) {
            node.expanded = true;
            this.part_layout(node);
            this.set_visible(node.children, true);
        },

        collapse_node: function (node) {
            node.expanded = false;
            this.part_layout(node);
            this.set_visible(node.children, false);
        },

        expand_all: function () {
            var nodes = this.jm.mind.nodes;
            var c = 0;
            var node;
            for (var nodeid in nodes) {
                node = nodes[nodeid];
                if (!node.expanded) {
                    node.expanded = true;
                    c++;
                }
            }
            if (c > 0) {
                var root = this.jm.mind.root;
                this.part_layout(root);
                this.set_visible(root.children, true);
            }
        },

        collapse_all: function () {
            var nodes = this.jm.mind.nodes;
            var c = 0;
            var node;
            for (var nodeid in nodes) {
                node = nodes[nodeid];
                if (node.expanded && !node.isroot) {
                    node.expanded = false;
                    c++;
                }
            }
            if (c > 0) {
                var root = this.jm.mind.root;
                this.part_layout(root);
                this.set_visible(root.children, true);
            }
        },

        expand_to_depth: function (target_depth, curr_nodes, curr_depth) {
            if (target_depth < 1) {
                return;
            }
            var nodes = curr_nodes || this.jm.mind.root.children;
            var depth = curr_depth || 1;
            var i = nodes.length;
            var node = null;
            while (i--) {
                node = nodes[i];
                if (depth < target_depth) {
                    if (!node.expanded) {
                        this.expand_node(node);
                    }
                    this.expand_to_depth(
                        target_depth,
                        node.children,
                        depth + 1
                    );
                }
                if (depth == target_depth) {
                    if (node.expanded) {
                        this.collapse_node(node);
                    }
                }
            }
        },

        part_layout: function (node) {
            var root = this.jm.mind.root;
            if (!!root) {
                var root_layout_data = root._data.layout;
                if (node.isroot) {
                    root_layout_data.outer_height_right =
                        this._layout_offset_subnodes_height(
                            root_layout_data.right_nodes
                        );
                    root_layout_data.outer_height_left =
                        this._layout_offset_subnodes_height(
                            root_layout_data.left_nodes
                        );
                } else {
                    if (node._data.layout.direction == jm.direction.right) {
                        root_layout_data.outer_height_right =
                            this._layout_offset_subnodes_height(
                                root_layout_data.right_nodes
                            );
                    } else {
                        root_layout_data.outer_height_left =
                            this._layout_offset_subnodes_height(
                                root_layout_data.left_nodes
                            );
                    }
                }
                this.bounds.s = Math.max(
                    root_layout_data.outer_height_left,
                    root_layout_data.outer_height_right
                );
                this.cache_valid = false;
            } else {
                logger.warn("can not found root node");
            }
        },

        set_visible: function (nodes, visible) {
            var i = nodes.length;
            var node = null;
            var layout_data = null;
            while (i--) {
                node = nodes[i];
                layout_data = node._data.layout;
                if (node.expanded) {
                    this.set_visible(node.children, visible);
                } else {
                    this.set_visible(node.children, false);
                }
                if (!node.isroot) {
                    node._data.layout.visible = visible;
                }
            }
        },

        is_expand: function (node) {
            return node.expanded;
        },

        is_visible: function (node) {
            var layout_data = node._data.layout;
            if ("visible" in layout_data && !layout_data.visible) {
                return false;
            } else {
                return true;
            }
        },
    };

    // view provider
    jm.view_provider = function (jm, options) {
        this.opts = options;
        this.jm = jm;
        this.layout = jm.layout;

        this.container = null;
        this.e_panel = null;
        this.e_nodes = null;
        this.e_canvas = null;

        this.canvas_ctx = null;
        this.size = { w: 0, h: 0 };

        this.selected_node = null;
        this.editing_node = null;
    };

    jm.view_provider.prototype = {
        init: function () {
            logger.debug("view.init");

            this.container = $i(this.opts.container)
                ? this.opts.container
                : $g(this.opts.container);
            if (!this.container) {
                logger.error(
                    "the options.view.container was not be found in dom"
                );
                return;
            }
            this.e_panel = $c("div");
            this.e_canvas = $c("canvas");
            this.e_nodes = $c("jmnodes");
            this.e_editor = $c("input");

            this.e_panel.className = "jsmind-inner";
            this.e_panel.appendChild(this.e_canvas);
            this.e_panel.appendChild(this.e_nodes);

            this.e_editor.className = "jsmind-editor";
            this.e_editor.type = "text";

            this.actualZoom = 1;
            this.zoomStep = 0.1;
            this.minZoom = 0.5;
            this.maxZoom = 2;

            var v = this;
            jm.util.dom.add_event(this.e_editor, "keydown", function (e) {
                var evt = e || event;
                if (evt.keyCode == 13) {
                    v.edit_node_end();
                    evt.stopPropagation();
                }
            });
            jm.util.dom.add_event(this.e_editor, "blur", function (e) {
                v.edit_node_end();
            });

            this.container.appendChild(this.e_panel);

            this.init_canvas();
        },

        add_event: function (obj, event_name, event_handle) {
            jm.util.dom.add_event(this.e_nodes, event_name, function (e) {
                var evt = e || event;
                event_handle.call(obj, evt);
            });
        },

        get_binded_nodeid: function (element) {
            if (element == null) {
                return null;
            }
            var tagName = element.tagName.toLowerCase();
            if (
                tagName == "jmnodes" ||
                tagName == "body" ||
                tagName == "html"
            ) {
                return null;
            }
            if (tagName == "jmnode" || tagName == "jmexpander") {
                return element.getAttribute("nodeid");
            } else {
                return this.get_binded_nodeid(element.parentElement);
            }
        },

        is_expander: function (element) {
            return element.tagName.toLowerCase() == "jmexpander";
        },

        reset: function () {
            logger.debug("view.reset");
            this.selected_node = null;
            this.clear_lines();
            this.clear_nodes();
            this.reset_theme();
        },

        reset_theme: function () {
            var theme_name = this.jm.options.theme;
            if (!!theme_name) {
                this.e_nodes.className = "theme-" + theme_name;
            } else {
                this.e_nodes.className = "";
            }
        },

        reset_custom_style: function () {
            var nodes = this.jm.mind.nodes;
            for (var nodeid in nodes) {
                this.reset_node_custom_style(nodes[nodeid]);
            }
        },

        load: function () {
            logger.debug("view.load");
            this.init_nodes();
        },

        expand_size: function () {
            var min_size = this.layout.get_min_size();
            var min_width = min_size.w + this.opts.hmargin * 2;
            var min_height = min_size.h + this.opts.vmargin * 2;
            var client_w = this.e_panel.clientWidth;
            var client_h = this.e_panel.clientHeight;
            if (client_w < min_width) {
                client_w = min_width;
            }
            if (client_h < min_height) {
                client_h = min_height;
            }
            this.size.w = client_w;
            this.size.h = client_h;
        },

        init_canvas: function () {
            var ctx = this.e_canvas.getContext("2d");
            this.canvas_ctx = ctx;
        },

        init_nodes_size: function (node) {
            var view_data = node._data.view;
            view_data.width = view_data.element.clientWidth;
            view_data.height = view_data.element.clientHeight;
        },

        init_nodes: function () {
            var nodes = this.jm.mind.nodes;
            var doc_frag = $d.createDocumentFragment();
            for (var nodeid in nodes) {
                this.create_node_element(nodes[nodeid], doc_frag);
            }
            this.e_nodes.appendChild(doc_frag);
            for (var nodeid in nodes) {
                this.init_nodes_size(nodes[nodeid]);
            }
        },

        add_node: function (node) {
            this.create_node_element(node, this.e_nodes);
            this.init_nodes_size(node);
        },

        create_node_element: function (node, parent_node) {
            var view_data = null;
            if ("view" in node._data) {
                view_data = node._data.view;
            } else {
                view_data = {};
                node._data.view = view_data;
            }

            var d = $c("jmnode");
            if (node.isroot) {
                d.className = "root";
            } else {
                var d_e = $c("jmexpander");
                $t(d_e, "-");
                d_e.setAttribute("nodeid", node.id);
                d_e.style.visibility = "hidden";
                parent_node.appendChild(d_e);
                view_data.expander = d_e;
            }
            if (!!node.topic) {
                if (this.opts.support_html) {
                    $h(d, node.topic);
                } else {
                    $t(d, node.topic);
                }
            }
            d.setAttribute("nodeid", node.id);
            d.style.visibility = "hidden";
            this._reset_node_custom_style(d, node.data);

            parent_node.appendChild(d);
            view_data.element = d;
        },

        remove_node: function (node) {
            if (
                this.selected_node != null &&
                this.selected_node.id == node.id
            ) {
                this.selected_node = null;
            }
            if (this.editing_node != null && this.editing_node.id == node.id) {
                node._data.view.element.removeChild(this.e_editor);
                this.editing_node = null;
            }
            var children = node.children;
            var i = children.length;
            while (i--) {
                this.remove_node(children[i]);
            }
            if (node._data.view) {
                var element = node._data.view.element;
                var expander = node._data.view.expander;
                this.e_nodes.removeChild(element);
                this.e_nodes.removeChild(expander);
                node._data.view.element = null;
                node._data.view.expander = null;
            }
        },

        update_node: function (node) {
            var view_data = node._data.view;
            var element = view_data.element;
            if (!!node.topic) {
                if (this.opts.support_html) {
                    $h(element, node.topic);
                } else {
                    $t(element, node.topic);
                }
            }
            view_data.width = element.clientWidth;
            view_data.height = element.clientHeight;
        },

        select_node: function (node) {
            if (!!this.selected_node) {
                this.selected_node._data.view.element.className =
                    this.selected_node._data.view.element.className.replace(
                        /\s*selected\b/i,
                        ""
                    );
                this.reset_node_custom_style(this.selected_node);
            }
            if (!!node) {
                this.selected_node = node;
                node._data.view.element.className += " selected";
                this.clear_node_custom_style(node);
            }
        },

        select_clear: function () {
            this.select_node(null);
        },

        get_editing_node: function () {
            return this.editing_node;
        },

        is_editing: function () {
            return !!this.editing_node;
        },

        edit_node_begin: function (node) {
            if (!node.topic) {
                logger.warn("don't edit image nodes");
                return;
            }
            if (this.editing_node != null) {
                this.edit_node_end();
            }
            this.editing_node = node;
            var view_data = node._data.view;
            var element = view_data.element;
            var topic = node.topic;
            var ncs = getComputedStyle(element);
            this.e_editor.value = topic;
            this.e_editor.style.width =
                element.clientWidth -
                parseInt(ncs.getPropertyValue("padding-left")) -
                parseInt(ncs.getPropertyValue("padding-right")) +
                "px";
            element.innerHTML = "";
            element.appendChild(this.e_editor);
            element.style.zIndex = 5;
            this.e_editor.focus();
            this.e_editor.select();
        },

        edit_node_end: function () {
            if (this.editing_node != null) {
                var node = this.editing_node;
                this.editing_node = null;
                var view_data = node._data.view;
                var element = view_data.element;
                var topic = this.e_editor.value;
                element.style.zIndex = "auto";
                element.removeChild(this.e_editor);
                if (jm.util.text.is_empty(topic) || node.topic === topic) {
                    if (this.opts.support_html) {
                        $h(element, node.topic);
                    } else {
                        $t(element, node.topic);
                    }
                } else {
                    this.jm.update_node(node.id, topic);
                }
            }
        },

        get_view_offset: function () {
            var bounds = this.layout.bounds;
            var _x = (this.size.w - bounds.e - bounds.w) / 2;
            var _y = this.size.h / 2;
            return { x: _x, y: _y };
        },

        resize: function () {
            this.e_canvas.width = 1;
            this.e_canvas.height = 1;
            this.e_nodes.style.width = "1px";
            this.e_nodes.style.height = "1px";

            this.expand_size();
            this._show();
        },

        _show: function () {
            this.e_canvas.width = this.size.w;
            this.e_canvas.height = this.size.h;
            this.e_nodes.style.width = this.size.w + "px";
            this.e_nodes.style.height = this.size.h + "px";
            this.show_nodes();
            this.show_lines();
            //this.layout.cache_valid = true;
            this.jm.invoke_event_handle(jm.event_type.resize, { data: [] });
        },

        zoomIn: function () {
            return this.setZoom(this.actualZoom + this.zoomStep);
        },

        zoomOut: function () {
            return this.setZoom(this.actualZoom - this.zoomStep);
        },

        setZoom: function (zoom) {
            if (zoom < this.minZoom || zoom > this.maxZoom) {
                return false;
            }
            this.actualZoom = zoom;
            for (var i = 0; i < this.e_panel.children.length; i++) {
                // this.e_panel.children[i].style.transform = 'scale(' + zoom + ')';
                this.e_panel.children[i].style.zoom = zoom;
            }
            this.show(true);
            return true;
        },

        _center_root: function () {
            // center root node
            var outer_w = this.e_panel.clientWidth;
            var outer_h = this.e_panel.clientHeight;
            if (this.size.w > outer_w) {
                var _offset = this.get_view_offset();
                this.e_panel.scrollLeft = _offset.x - outer_w / 2;
            }
            if (this.size.h > outer_h) {
                this.e_panel.scrollTop = (this.size.h - outer_h) / 2;
            }
        },

        show: function (keep_center) {
            logger.debug("view.show");
            this.expand_size();
            this._show();
            if (!!keep_center) {
                this._center_root();
            }
        },

        relayout: function () {
            this.expand_size();
            this._show();
        },

        save_location: function (node) {
            var vd = node._data.view;
            vd._saved_location = {
                x: parseInt(vd.element.style.left) - this.e_panel.scrollLeft,
                y: parseInt(vd.element.style.top) - this.e_panel.scrollTop,
            };
        },

        restore_location: function (node) {
            var vd = node._data.view;
            this.e_panel.scrollLeft =
                parseInt(vd.element.style.left) - vd._saved_location.x;
            this.e_panel.scrollTop =
                parseInt(vd.element.style.top) - vd._saved_location.y;
        },

        clear_nodes: function () {
            var mind = this.jm.mind;
            if (mind == null) {
                return;
            }
            var nodes = mind.nodes;
            var node = null;
            for (var nodeid in nodes) {
                node = nodes[nodeid];
                node._data.view.element = null;
                node._data.view.expander = null;
            }
            this.e_nodes.innerHTML = "";
        },

        show_nodes: function () {
            var nodes = this.jm.mind.nodes;
            var node = null;
            var node_element = null;
            var expander = null;
            var p = null;
            var p_expander = null;
            var expander_text = "-";
            var view_data = null;
            var _offset = this.get_view_offset();
            for (var nodeid in nodes) {
                node = nodes[nodeid];
                view_data = node._data.view;
                node_element = view_data.element;
                expander = view_data.expander;
                if (!this.layout.is_visible(node)) {
                    node_element.style.display = "none";
                    expander.style.display = "none";
                    continue;
                }
                this.reset_node_custom_style(node);
                p = this.layout.get_node_point(node);
                view_data.abs_x = _offset.x + p.x;
                view_data.abs_y = _offset.y + p.y;
                node_element.style.left = _offset.x + p.x + "px";
                node_element.style.top = _offset.y + p.y + "px";
                node_element.style.display = "";
                node_element.style.visibility = "visible";
                if (!node.isroot && node.children.length > 0) {
                    expander_text = node.expanded ? "-" : "+";
                    p_expander = this.layout.get_expander_point(node);
                    expander.style.left = _offset.x + p_expander.x + "px";
                    expander.style.top = _offset.y + p_expander.y + "px";
                    expander.style.display = "";
                    expander.style.visibility = "visible";
                    $t(expander, expander_text);
                }
                // hide expander while all children have been removed
                if (!node.isroot && node.children.length == 0) {
                    expander.style.display = "none";
                    expander.style.visibility = "hidden";
                }
            }
        },

        reset_node_custom_style: function (node) {
            this._reset_node_custom_style(node._data.view.element, node.data);
        },

        _reset_node_custom_style: function (node_element, node_data) {
            if ("background-color" in node_data) {
                node_element.style.backgroundColor =
                    node_data["background-color"];
            }
            if ("foreground-color" in node_data) {
                node_element.style.color = node_data["foreground-color"];
            }
            if ("width" in node_data) {
                node_element.style.width = node_data["width"] + "px";
            }
            if ("height" in node_data) {
                node_element.style.height = node_data["height"] + "px";
            }
            if ("font-size" in node_data) {
                node_element.style.fontSize = node_data["font-size"] + "px";
            }
            if ("font-weight" in node_data) {
                node_element.style.fontWeight = node_data["font-weight"];
            }
            if ("font-style" in node_data) {
                node_element.style.fontStyle = node_data["font-style"];
            }
            if ("background-image" in node_data) {
                var backgroundImage = node_data["background-image"];
                if (
                    backgroundImage.startsWith("data") &&
                    node_data["width"] &&
                    node_data["height"]
                ) {
                    var img = new Image();

                    img.onload = function () {
                        var c = $c("canvas");
                        c.width = node_element.clientWidth;
                        c.height = node_element.clientHeight;
                        var img = this;
                        if (c.getContext) {
                            var ctx = c.getContext("2d");
                            ctx.drawImage(
                                img,
                                2,
                                2,
                                node_element.clientWidth,
                                node_element.clientHeight
                            );
                            var scaledImageData = c.toDataURL();
                            node_element.style.backgroundImage =
                                "url(" + scaledImageData + ")";
                        }
                    };
                    img.src = backgroundImage;
                } else {
                    node_element.style.backgroundImage =
                        "url(" + backgroundImage + ")";
                }
                node_element.style.backgroundSize = "99%";

                if ("background-rotation" in node_data) {
                    node_element.style.transform =
                        "rotate(" + node_data["background-rotation"] + "deg)";
                }
            }
        },

        clear_node_custom_style: function (node) {
            var node_element = node._data.view.element;
            node_element.style.backgroundColor = "";
            node_element.style.color = "";
        },

        clear_lines: function (canvas_ctx) {
            var ctx = canvas_ctx || this.canvas_ctx;
            jm.util.canvas.clear(ctx, 0, 0, this.size.w, this.size.h);
        },

        show_lines: function (canvas_ctx) {
            this.clear_lines(canvas_ctx);
            var nodes = this.jm.mind.nodes;
            var node = null;
            var pin = null;
            var pout = null;
            var _offset = this.get_view_offset();
            for (var nodeid in nodes) {
                node = nodes[nodeid];
                if (!!node.isroot) {
                    continue;
                }
                if (
                    "visible" in node._data.layout &&
                    !node._data.layout.visible
                ) {
                    continue;
                }
                pin = this.layout.get_node_point_in(node);
                pout = this.layout.get_node_point_out(node.parent);
                this.draw_line(pout, pin, _offset, canvas_ctx);
            }
        },

        draw_line: function (pin, pout, offset, canvas_ctx) {
            var ctx = canvas_ctx || this.canvas_ctx;
            ctx.strokeStyle = this.opts.line_color;
            ctx.lineWidth = this.opts.line_width;
            ctx.lineCap = "round";

            jm.util.canvas.bezierto(
                ctx,
                pin.x + offset.x,
                pin.y + offset.y,
                pout.x + offset.x,
                pout.y + offset.y
            );
        },
    };

    // shortcut provider
    jm.shortcut_provider = function (jm, options) {
        this.jm = jm;
        this.opts = options;
        this.mapping = options.mapping;
        this.handles = options.handles;
        this._mapping = {};
    };

    jm.shortcut_provider.prototype = {
        init: function () {
            jm.util.dom.add_event($d, "keydown", this.handler.bind(this));

            this.handles["addchild"] = this.handle_addchild;
            this.handles["addbrother"] = this.handle_addbrother;
            this.handles["editnode"] = this.handle_editnode;
            this.handles["delnode"] = this.handle_delnode;
            this.handles["toggle"] = this.handle_toggle;
            this.handles["up"] = this.handle_up;
            this.handles["down"] = this.handle_down;
            this.handles["left"] = this.handle_left;
            this.handles["right"] = this.handle_right;

            for (var handle in this.mapping) {
                if (!!this.mapping[handle] && handle in this.handles) {
                    this._mapping[this.mapping[handle]] = this.handles[handle];
                }
            }
        },

        enable_shortcut: function () {
            this.opts.enable = true;
        },

        disable_shortcut: function () {
            this.opts.enable = false;
        },

        handler: function (e) {
            if (this.jm.view.is_editing()) {
                return;
            }
            var evt = e || event;
            if (!this.opts.enable) {
                return true;
            }
            var kc = evt.keyCode;
            if (kc in this._mapping) {
                this._mapping[kc].call(this, this.jm, e);
            }
        },

        handle_addchild: function (_jm, e) {
            var selected_node = _jm.get_selected_node();
            if (!!selected_node) {
                var nodeid = jm.util.uuid.newid();
                var node = _jm.add_node(selected_node, nodeid, "New Node");
                if (!!node) {
                    _jm.select_node(nodeid);
                    _jm.begin_edit(nodeid);
                }
            }
        },
        handle_addbrother: function (_jm, e) {
            var selected_node = _jm.get_selected_node();
            if (!!selected_node && !selected_node.isroot) {
                var nodeid = jm.util.uuid.newid();
                var node = _jm.insert_node_after(
                    selected_node,
                    nodeid,
                    "New Node"
                );
                if (!!node) {
                    _jm.select_node(nodeid);
                    _jm.begin_edit(nodeid);
                }
            }
        },
        handle_editnode: function (_jm, e) {
            var selected_node = _jm.get_selected_node();
            if (!!selected_node) {
                _jm.begin_edit(selected_node);
            }
        },
        handle_delnode: function (_jm, e) {
            var selected_node = _jm.get_selected_node();
            if (!!selected_node && !selected_node.isroot) {
                _jm.select_node(selected_node.parent);
                _jm.remove_node(selected_node);
            }
        },
        handle_toggle: function (_jm, e) {
            var evt = e || event;
            var selected_node = _jm.get_selected_node();
            if (!!selected_node) {
                _jm.toggle_node(selected_node.id);
                evt.stopPropagation();
                evt.preventDefault();
            }
        },
        handle_up: function (_jm, e) {
            var evt = e || event;
            var selected_node = _jm.get_selected_node();
            if (!!selected_node) {
                var up_node = _jm.find_node_before(selected_node);
                if (!up_node) {
                    var np = _jm.find_node_before(selected_node.parent);
                    if (!!np && np.children.length > 0) {
                        up_node = np.children[np.children.length - 1];
                    }
                }
                if (!!up_node) {
                    _jm.select_node(up_node);
                }
                evt.stopPropagation();
                evt.preventDefault();
            }
        },

        handle_down: function (_jm, e) {
            var evt = e || event;
            var selected_node = _jm.get_selected_node();
            if (!!selected_node) {
                var down_node = _jm.find_node_after(selected_node);
                if (!down_node) {
                    var np = _jm.find_node_after(selected_node.parent);
                    if (!!np && np.children.length > 0) {
                        down_node = np.children[0];
                    }
                }
                if (!!down_node) {
                    _jm.select_node(down_node);
                }
                evt.stopPropagation();
                evt.preventDefault();
            }
        },

        handle_left: function (_jm, e) {
            this._handle_direction(_jm, e, jm.direction.left);
        },
        handle_right: function (_jm, e) {
            this._handle_direction(_jm, e, jm.direction.right);
        },
        _handle_direction: function (_jm, e, d) {
            var evt = e || event;
            var selected_node = _jm.get_selected_node();
            var node = null;
            if (!!selected_node) {
                if (selected_node.isroot) {
                    var c = selected_node.children;
                    var children = [];
                    for (var i = 0; i < c.length; i++) {
                        if (c[i].direction === d) {
                            children.push(i);
                        }
                    }
                    node = c[children[Math.floor((children.length - 1) / 2)]];
                } else if (selected_node.direction === d) {
                    var children = selected_node.children;
                    var childrencount = children.length;
                    if (childrencount > 0) {
                        node = children[Math.floor((childrencount - 1) / 2)];
                    }
                } else {
                    node = selected_node.parent;
                }
                if (!!node) {
                    _jm.select_node(node);
                }
                evt.stopPropagation();
                evt.preventDefault();
            }
        },
    };

    // plugin
    jm.plugin = function (name, init) {
        this.name = name;
        this.init = init;
    };

    jm.plugins = [];

    jm.register_plugin = function (plugin) {
        if (plugin instanceof jm.plugin) {
            jm.plugins.push(plugin);
        }
    };

    jm.init_plugins = function (sender) {
        $w.setTimeout(function () {
            jm._init_plugins(sender);
        }, 0);
    };

    jm._init_plugins = function (sender) {
        var l = jm.plugins.length;
        var fn_init = null;
        for (var i = 0; i < l; i++) {
            fn_init = jm.plugins[i].init;
            if (typeof fn_init === "function") {
                fn_init(sender);
            }
        }
    };

    // quick way
    jm.show = function (options, mind) {
        var _jm = new jm(options);
        _jm.show(mind);
        return _jm;
    };

    // export jsmind
    if (typeof module !== "undefined" && typeof exports === "object") {
        module.exports = jm;
    } else if (typeof define === "function" && (define.amd || define.cmd)) {
        define(function () {
            return jm;
        });
    } else {
        $w[__name__] = jm;
    }
})(typeof window !== "undefined" ? window : global);
